Skip to content

Make MLX metallib generation automatic and stale libraries loudly detectable - #131

Merged
a-github-name merged 4 commits into
mainfrom
fix/mlx-metallib-staleness
Jul 3, 2026
Merged

Make MLX metallib generation automatic and stale libraries loudly detectable#131
a-github-name merged 4 commits into
mainfrom
fix/mlx-metallib-staleness

Conversation

@a-github-name

Copy link
Copy Markdown
Member

Incident

On 2026-07-03, text chat at temperature 0 produced incoherent multilingual gibberish with run-to-run nondeterminism on any prompt past ~1024 tokens of total context, on every MLX text model (Q35/ornith, q36-nano, and by the same mechanism Gemma4/LFM2/Psi), while short prompts stayed perfectly clean. GGUF/llama.cpp models were unaffected.

Root cause

swift build never compiles mlx-swift's Metal kernels — there is no metallib rule in the SwiftPM manifest — yet the MLX runtime hard-requires default.metallib and loads whatever file it finds by name, with no version validation. The repo has vendored a copy (vendor/mlx-swift_Cmlx.bundle, tracked in git and consulted first by both the CLI bundle resolver and the test harness) that was built from pre-0.30 mlx sources at the initial public release and never regenerated.

mlx 0.31.x rewrote the 2-pass sdpa_vector kernel ABI (ml-explore/mlx#3023: bf16 partials instead of fp32, gqa_factor buffer removed, new blocks function constant). The 1-pass kernel ABI didn't change — so everything below 1024 keys worked — but the moment single-token decode crossed the k_len >= 1024 two-pass dispatch threshold ('d'/'s'-class Apple GPUs, e.g. M4 Max), the 0.31.1 host code drove pre-0.30 kernel binaries: NaN/garbage partials, nondeterministic output. Isolated probe on the exact decode shape (q [1,16,1,256] bf16, GQA 16:2, cache-style strided k/v views): clean at N=1023, ~1e34/NaN and run-to-run drift at N≥1024 with the stale lib; drift 0.0 and ref error ≤2e-4 at all N with a freshly compiled one.

Skip-if-exists copy/symlink logic in MLXBundleSupport, MLXTestSupport, and install.sh then propagated the stale blob into every build dir, install, xctest bundle, and release staging tree indefinitely.

Changes

  • scripts/build_mlx_metallib.sh — compiles the AOT .metal sources from the current mlx-swift checkout with mlx's mandatory -fno-fast-math flags, links default.metallib, and writes a default.metallib.version sidecar (mlx core version, mlx-swift pin revision, kernel-source SHA-256). Installs into .build (both configs) and refreshes the tracked vendor bundle. --verify-only <bundle> exits non-zero on any drift from the checkout.
  • vendor/mlx-swift_Cmlx.bundle — the tracked metallib regenerated from mlx-swift 0.31.4 (mlx core 0.31.1), now with its version stamp.
  • Runtime validationMLXRuntimeVersion (MereRunCore) exposes the compiled-in mlx core version via mlx-c; MLXBundleSupport.ensureAvailable compares it against the bundle stamp at startup: hard error on mismatch (override: MERERUN_ALLOW_METALLIB_MISMATCH=1), loud warning when unstamped, self-heal by preferring stamp-matched candidate bundles, atomic always-fresh compatibility copies (previously skip-if-exists, which let stale flat copies outlive bundle updates), and a bundle husk without a metallib no longer counts as present.
  • MLXTestSupport — replaces stale colocated symlinks/copies instead of skipping them. A leftover absolute symlink into another checkout's vendored bundle had pinned test runs to the broken metallib; steady-state (symlink already pointing at the current source) remains a no-op.
  • SDPAVectorKernelTests — GPU regression test on the exact failing shape across the 1024-key dispatch boundary (1023 control / 1100 / 3300): unfused-fp32 agreement plus bit-identical determinism over 3 runs. Fails against a stale metallib (verified live against two independent stale copies), passes against a stamped fresh one. Run with MERERUN_TEST_MLX_DEVICE=gpu.
  • Packagingbuild_mere_run_app.sh rebuilds + stamps the metallib after swift build and refuses to package a bundle failing --verify-only; install.sh carries the stamp alongside the compatibility copies and warns when installing an unstamped library.

Verification

  • All four runtime validation paths exercised on a real binary: matched (silent), mismatched (hard error naming both versions), override (warning + run), unstamped (loud warning).
  • Original failing repro (3.3k-token prompt, ornith-35b-mlx, temp 0): run-to-run identical, coherent output — release CLI and the refreshed installed CLI (via q36-nano) both verified.
  • MERERUN_TEST_MLX_DEVICE=gpu swift test --filter "SDPAVectorKernelTests|Gemma4DecodeFusedKernelsTests": 6 tests, 0 failures.
  • --verify-only green on the committed vendor bundle; red on a tampered stamp.

Notes for reviewers

  • The vendor metallib is a binary diff (3.83 MB → 2.86 MB); the sidecar records exactly what it was built from. It must be regenerated (script does it automatically) on every mlx-swift bump — the new runtime check turns forgetting into a hard error instead of silent corruption.
  • The release-tools packaging pipeline stages its own vendored bundle copies and should adopt build_mlx_metallib.sh --verify-only; that lives in the private repo and is not covered here.
  • Pre-existing and unrelated: swift test -c release fails to build (seedLoadedModelForTesting is debug-only).
  • CI suggestion: run the regression test on an Apple Silicon runner with MERERUN_TEST_MLX_DEVICE=gpu.

🤖 Generated with Claude Code

a-github-name and others added 4 commits July 3, 2026 17:11
…ectable

The AOT Metal kernel library (default.metallib) is not produced by `swift
build` — there is no metallib rule in the SwiftPM manifest — yet the MLX
runtime hard-requires it and loads whatever file it finds by name. The repo
has vendored a copy (vendor/mlx-swift_Cmlx.bundle, consulted FIRST by both
the CLI and test bundle resolvers) built from pre-0.30 mlx sources since the
initial public release. mlx 0.31.x rewrote the 2-pass sdpa_vector kernel ABI,
so once single-token decode crossed 1024 keys the mismatched kernels returned
garbage, nondeterministically at temperature 0, for every MLX text model
(Q35/ornith, Gemma4, LFM2, Psi) — while short prompts stayed clean
(2026-07-03 incident).

- scripts/build_mlx_metallib.sh: compile the AOT .metal sources from the
  current mlx-swift checkout with mlx's mandatory -fno-fast-math flags, link
  default.metallib, and stamp it (default.metallib.version) with the mlx core
  version, mlx-swift pin, and a kernel-source hash. Installs into .build
  (both configs) and refreshes the tracked vendor bundle; --verify-only
  checks an existing bundle against the checkout.
- vendor/mlx-swift_Cmlx.bundle: regenerate the tracked metallib from
  mlx-swift 0.31.4 (mlx core 0.31.1) and add its version stamp.
- MLXBundleSupport: validate the stamp at startup — hard error on mismatch
  (MERERUN_ALLOW_METALLIB_MISMATCH=1 override), loud warning when unstamped,
  self-heal by preferring stamp-matched candidate bundles, and atomically
  refresh the flat compatibility copies instead of skipping existing (stale)
  ones. A bundle husk without a metallib no longer counts as present.
  MLXRuntimeVersion (MereRunCore) exposes the compiled-in mlx core version
  via mlx-c for the comparison.
- MLXTestSupport: replace stale colocated symlinks/copies instead of
  skipping them — a leftover absolute symlink into another checkout's
  vendored bundle pinned test runs to the broken metallib.
- SDPAVectorKernelTests: GPU regression test on the exact failing shape
  (q [1,16,1,256] bf16, GQA 16:2, cache-style strided k/v views) across the
  1024-key 2-pass dispatch boundary: unfused-fp32 agreement plus
  bit-identical determinism across repeated runs. Fails against the stale
  metallib, passes against a stamped fresh one. Run with
  MERERUN_TEST_MLX_DEVICE=gpu.
- build_mere_run_app.sh: rebuild + stamp the metallib after swift build and
  refuse to package a bundle that fails --verify-only. install.sh: carry the
  stamp alongside the compatibility copies and warn when installing an
  unstamped library.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plain `swift build` emits no mlx-swift_Cmlx.bundle on CI, so the metallib
script's mkdir-created skeleton lacked Contents/Info.plist and the app's
deep codesign seal rejected the bundle ("bundle format unrecognized,
invalid, or unsuitable"). Write a minimal resource-bundle Info.plist
whenever the bundle doesn't already have one. Verified locally by
deleting the bundle and running scripts/build_mere_run_app.sh debug
end-to-end (codesign --verify passes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant